///
/// ArmyBodger 3
/// (c)2010 richyp
///
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml.Linq;
using System.Xml.XPath;
using Ionic.Zip;
using ArmyBodger.Core.Utilities;

namespace ArmyBodger.Core {
    /// <summary>
    /// This class parses the Army Book Zip files and creates an Army Book object
    /// </summary>
    public class ArmyBookParser {
        /// <summary>
        /// Collection of Bodgers supported Game Systems
        /// </summary>
        public GameSystemCollection GameSystems { get; set; }
        /// <summary>
        /// An instance of the EquipmentParser class to read the equipment files
        /// provided by the Army Book
        /// </summary>
        public EquipmentParser EquipmentParser { get; set; }
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="gameSystems">A Collection of supported game systems</param>
        public ArmyBookParser(GameSystemCollection gameSystems) {
            this.GameSystems = gameSystems;
            this.EquipmentParser = new EquipmentParser();
        }
        /// <summary>
        /// Load an army book zip file from the specified location
        /// </summary>
        /// <param name="armyBooksPath">The root directory for all Army Books</param>
        /// <param name="file">The name of the Zip file to parse</param>
        /// <param name="tempDirectory">The location to extract the content too</param>
        /// <returns>A fully parsed ArmyBook object</returns>
        public ArmyBook ParseArmyBookFile(string armyBooksPath, string file, string tempDirectory) {
            string filename = armyBooksPath + "/" + file;
            if (!File.Exists(filename) && !Directory.Exists(filename) ) {
                throw new Exception("No such file: " + filename);
            }
            ArmyBook armyBook = new ArmyBook();
            string extractPath = tempDirectory + "/" + file;
            string gameSystemName = file.Substring(0, file.IndexOf("/"));

            // found a zipped codex
            if (file.EndsWith(".zip"))
            {
                try
                {
                    Directory.CreateDirectory(tempDirectory + "/" + file.Substring(0, file.IndexOf("/")));
                    Directory.CreateDirectory(extractPath);
                    using (ZipFile zf = ZipFile.Read(filename))
                    {
                        zf.ExtractAll(extractPath, ExtractExistingFileAction.OverwriteSilently);
                    }
                }
                catch (Exception e)
                {
                    throw e;
                }

                extractPath += "/" + file.Substring(file.IndexOf("/") + 1);
                extractPath = extractPath.Substring(0, extractPath.LastIndexOf("."));
            }

            else
            {
                extractPath = "Data/Army Books/" + file;
            }

            if (!File.Exists(extractPath + "/index.xml")) {
                System.Windows.Forms.MessageBox.Show("File \"index.xml\" cannot be found in unit " + filename);
                return null;
                //throw new Exception(file + " is not a valid army book file");
            }
            XDocument doc = null;
            try {
                doc = XDocument.Load(extractPath + "/index.xml");
            } catch {
                throw new Exception(file + " is not a valid XML file");
            }
            if (this.GameSystems[gameSystemName] == null) {
                throw new Exception(gameSystemName + " is not a supported game system.");
            }
            if (doc.Root.Name != "armyBook") {
                throw new Exception(file + " is XML but not an army book index file");
            }
            GameSystem gs = this.GameSystems[gameSystemName];
            armyBook.Header.ExtractPath = extractPath;
            this.ParseHeader(armyBook, doc, gs);
            this.LoadScripts(armyBook, doc, gs);
            this.ParseEquipment(armyBook, doc, gs);
            this.ParseBaseUnits(armyBook, doc, gs);
            this.ParseEntries(armyBook, doc, gs);
            return armyBook;
        }
        /// <summary>
        /// Parse the dynamic header object
        /// </summary>
        /// <param name="armyBook">The Army Book the header belongs to</param>
        /// <param name="doc">The Index XML Document</param>
        /// <param name="gs">The Game System used by this Army Book</param>
        protected void ParseHeader(ArmyBook armyBook, XDocument doc, GameSystem gs) {
            foreach (XElement element in doc.XPathSelectElements("armyBook/header/*")) {
                armyBook.Header.Properties.Add(Helpers.ConvertToSentenceCase(element.Name.ToString()), element.Value);
            }
            armyBook.Header.Properties.Add("GameSystem", gs.Name);
        }
        /// <summary>
        /// Load the provided Python scripts from this Army Books scripts section
        /// </summary>
        /// <param name="armyBook">The Army Book the scripts belong to</param>
        /// <param name="doc">The Index XML Document</param>
        /// <param name="gs">The Game System used by this Army Book</param>
        protected void LoadScripts(ArmyBook armyBook, XDocument doc, GameSystem gs) {
            foreach (XElement element in doc.XPathSelectElements("armyBook/body/scripts/file")) {
                try {
                    if (File.Exists(armyBook.Header.ExtractPath + "/" + element.Value)) {
                        armyBook.ScriptFiles.Add(armyBook.Header.ExtractPath + "/" + element.Value);
                    }
                } catch {
                    Console.WriteLine("Error adding script file: " + element.Value);
                }
            }
        }
        /// <summary>
        /// Parse the equipment files provided by this Army Book
        /// </summary>
        /// <param name="armyBook">The Army Book to parse</param>
        /// <param name="doc">The Index XML Document</param>
        /// <param name="gs">The Game System used by this Army Book</param>
        protected void ParseEquipment(ArmyBook armyBook, XDocument doc, GameSystem gs) {
            foreach (XElement element in doc.XPathSelectElements("armyBook/body/equipment/file")) {
                try {
                    armyBook.Equipment.AddRange(this.EquipmentParser.ParseEquipmentFile(armyBook.Header.ExtractPath + "/" + element.Value));
                } catch {
                    Console.WriteLine("Warning: Failed to open equipment file: " + element.Value);
                }
            }
        }
        /// <summary>
        /// Parse the Base Units/Vehicle Templates from the provided Base Units files
        /// </summary>
        /// <param name="armyBook">The Army Book to parse</param>
        /// <param name="doc">The Index XML Document</param>
        /// <param name="gs">The Game System used by this Army Book</param>
        protected void ParseBaseUnits(ArmyBook armyBook, XDocument doc, GameSystem gs) {
            foreach (XElement element in doc.XPathSelectElements("armyBook/body/baseUnits/file")) {
                try {
                    XDocument baseDoc = XDocument.Load(armyBook.Header.ExtractPath + "/" + element.Value);
                    foreach (XElement baseElement in baseDoc.XPathSelectElements("baseUnits/baseUnit")) {
                        BaseUnit unit = new BaseUnit();
                        unit.Name = baseElement.Element("name").Value;
                        foreach (XElement attrElement in baseElement.XPathSelectElements("attributes/attribute")) {
                            unit.Attributes.Add(new Attribute(attrElement.Attribute("name").Value, int.Parse(attrElement.Attribute("value").Value)));
                        }
                        armyBook.BaseUnits.Add(unit);
                    }
                } catch {
                    Console.WriteLine("Warning: Could not open: " + element.Value);
                }
            }
        }
        /// <summary>
        /// Parse the Unit Entry files for this Army Book
        /// </summary>
        /// <param name="armyBook">The Army Book to be parsed</param>
        /// <param name="doc">The Index XML Document</param>
        /// <param name="gs">The Game System used by this Army Book</param>
        protected void ParseEntries(ArmyBook armyBook, XDocument doc, GameSystem gs) {
            foreach (XElement element in doc.XPathSelectElements("armyBook/body/entries/file")) {
                try {
                    XDocument entryDoc = XDocument.Load(armyBook.Header.ExtractPath + "/" + element.Value);
                    foreach (XElement entryElement in entryDoc.XPathSelectElements("entries/entry")) {
                        Entry entry = new Entry();
                        entry.Name = entryElement.Element("name").Value;
                        entry.Category = entryElement.Element("category").Value;
                        if (entryElement.Element("special") != null && (entryElement.Element("special").Value == "True" || entryElement.Element("special").Value == "False"))
                        {
                            entry.IsSpecial = bool.Parse(entryElement.Element("special").Value);
                        }
                        if (entryElement.Element("onAddEvent") != null)
                        {
                            entry.OnAddEvent = entryElement.Element("onAddEvent").Value;
                        }
                        this.ParseUpgrades(armyBook, entryElement, gs, entry);
                        armyBook.AvailableEntries.Add(entry);
                        this.ParseUnits(armyBook, entryElement, gs, entry);
                    }
                } catch {
                    Console.WriteLine("Warning: Could not open: " + element.Value);
                }
            }
        }
        /// <summary>
        /// Parse the unit upgrades provided by this Army Book
        /// </summary>
        /// <param name="armyBook">The Army Book to parse</param>
        /// <param name="entryElement">The XML Entity representing the unit Entry</param>
        /// <param name="gs">The Game System used by this Army Book</param>
        /// <param name="entry">The Entry object to attach the upgrade objects to</param>
        protected void ParseUpgrades(ArmyBook armyBook, XElement entryElement, GameSystem gs, Entry entry) {
            EquipmentCollection allEquipment = new EquipmentCollection();
            allEquipment.AddRange(gs.Globals);
            allEquipment.AddRange(armyBook.Equipment);

            foreach (XElement upgradeElement in entryElement.XPathSelectElements("upgrades/upgrade"))
            {
                string name = upgradeElement.Attribute("name").Value;
                Upgrade upgrade = new Upgrade();
                upgrade.Name = name;
                upgrade.Equipment = allEquipment[name];

                if (upgrade.Equipment == null)
                {
                    Console.WriteLine("Skipping unknown upgrade option: " + name + " in entry: " + entry.Name);
                    string unit = "UNKNOWN";
                    if (upgradeElement.Parent.Parent.Element("name") != null)
                        unit = upgradeElement.Parent.Parent.Element("name").Value;
                    else if (upgradeElement.Parent.Parent.Parent.Element("name") != null)
                        unit = upgradeElement.Parent.Parent.Parent.Element("name").Value;

                    System.Windows.Forms.MessageBox.Show("Upgrade \"" + name + "\" cannot be found in unit " + unit);
                    continue;
                }

                if (upgradeElement.Attribute("cost") != null)
                    upgrade.Cost = int.Parse(upgradeElement.Attribute("cost").Value);
                else
                    upgrade.Cost = 0;

                if (upgradeElement.Attribute("default") != null)
                    upgrade.Default = bool.Parse(upgradeElement.Attribute("default").Value);
                else
                    upgrade.Default = false;

                if (upgradeElement.Attribute("onUpgrade") != null)
                    upgrade.OnUpgradeEvent = upgradeElement.Attribute("onUpgrade").Value;
                
                if (upgradeElement.Attribute("applyToAll") != null)
                    upgrade.CostIsUnitWide = bool.Parse(upgradeElement.Attribute("applyToAll").Value);
                else
                    upgrade.CostIsUnitWide = false;

                if (upgrade.Default)
                    upgrade.Applied = true;

                entry.Upgrades.Add(upgrade);
            }
        }
        /// <summary>
        /// Parse the individual units used by an Entry
        /// </summary>
        /// <param name="armyBook">The Army Book to be parsed</param>
        /// <param name="entryElement">The Entry XElement from the XML File</param>
        /// <param name="gs">The Game System used by this Army Book</param>
        /// <param name="entry">The Entry object the units belong to</param>
        protected void ParseUnits(ArmyBook armyBook, XElement entryElement, GameSystem gs, Entry entry) {
            foreach (XElement unitElement in entryElement.XPathSelectElements("units/unit")) {
                Unit unit = new Unit();
                unit.Name = unitElement.Attribute("name").Value;
                unit.BaseUnit = armyBook.BaseUnits[unitElement.Attribute("base").Value];
                unit.BaseCost = int.Parse(unitElement.Attribute("cost").Value);
                unit.InitialQuantity = int.Parse(unitElement.Attribute("quantity").Value);
                unit.Minimum = int.Parse(unitElement.Attribute("minimum").Value);
                unit.Maximum = int.Parse(unitElement.Attribute("maximum").Value);
                unit.IsPrimary = false;
                if (unitElement.Attribute("isPrimary") != null) {
                    unit.IsPrimary = bool.Parse(unitElement.Attribute("isPrimary").Value);
                }
                if (unitElement.Attribute("replaces") != null) {
                    unit.Replaces = unitElement.Attribute("replaces").Value;
                }
                this.ParseUnitEquipment(armyBook, unitElement, gs, unit);
                entry.PossibleUnits.Add(unit);
            }
        }
        /// <summary>
        /// Parse the equipment used/available to the provided Unit object
        /// </summary>
        /// <param name="armyBook">The Army Book being parsed</param>
        /// <param name="unitElement">The Unit XElement from the XML file</param>
        /// <param name="gs">The Game System used by this Army Book</param>
        /// <param name="unit">The Unit object the equipment applies to</param>
        protected void ParseUnitEquipment(ArmyBook armyBook, XElement unitElement, GameSystem gs, Unit unit) {
            EquipmentCollection allEquipment = new EquipmentCollection();
            allEquipment.AddRange(gs.Globals);
            allEquipment.AddRange(armyBook.Equipment);
            foreach (XElement archtypeElement in unitElement.XPathSelectElements("equipment/archetype"))
            {
                string archetypeName = archtypeElement.Attribute("name").Value;
                EquipmentOptionCollection archetypeCollection = new EquipmentOptionCollection();                
                if (archetypeName == null)
                    continue;
                
                foreach (XElement equipmentElement in archtypeElement.Elements("item"))
                {
                    EquipmentOption eo = this.ParseEquipmentOption(equipmentElement, allEquipment);
                    if (eo != null)
                        archetypeCollection.Add(eo);
                }

                unit.Archetypes.Add(archetypeName, archetypeCollection);

            }

            foreach (XElement equipmentElement in unitElement.XPathSelectElements("equipment/item")) {
                EquipmentOption eo = this.ParseEquipmentOption(equipmentElement, allEquipment);
                if(eo != null)
                    unit.Equipment.Add(eo);
            }
        }
        /// <summary>
        /// Recursively Parse the equipment options
        /// </summary>
        /// <param name="equipmentElement">The XML tag of the equipment option</param>
        /// <param name="equipment">Collection of all available equipment</param>
        /// <returns>A heirarchy of EquipmentOption's</returns>
        protected EquipmentOption ParseEquipmentOption(XElement equipmentElement, EquipmentCollection equipment) {
            EquipmentOption option = new EquipmentOption();
            option.Cost = 0;
            option.Fixed = false;
            option.Default = false;
            string equipmentName = equipmentElement.Attribute("name").Value;
            option.Equipment = equipment[equipmentName];
            if (option.Equipment == null)
            {
                string unit = "UNKNOWN";
                if (equipmentElement.Parent.Parent.Attribute("name") != null)
                    unit = equipmentElement.Parent.Parent.Attribute("name").Value;
                else if (equipmentElement.Parent.Parent.Parent.Attribute("name") != null)
                    unit = equipmentElement.Parent.Parent.Parent.Attribute("name").Value;
                
                System.Windows.Forms.MessageBox.Show("Equipment \"" + equipmentName + "\" cannot be found in unit " + unit);
                return null;
            }
            if (equipmentElement.Attribute("cost") != null) {
                option.Cost = int.Parse(equipmentElement.Attribute("cost").Value);
            }
            if (equipmentElement.Attribute("category") != null) {
                option.OverridenCategoryName = equipmentElement.Attribute("category").Value;
            }
            if (equipmentElement.Attribute("fixed") != null) {
                option.Fixed = bool.Parse(equipmentElement.Attribute("fixed").Value);
            }
            if (equipmentElement.Attribute("default") != null) {
                option.Default = bool.Parse(equipmentElement.Attribute("default").Value);
            }
            if (equipmentElement.Attribute("twinLinked") != null) {
                option.TwinLinked = bool.Parse(equipmentElement.Attribute("twinLinked").Value);
            }
            if (equipmentElement.Attribute("onEquip") != null) {
                option.OnEquipEvent = equipmentElement.Attribute("onEquip").Value;
            }
            if (equipmentElement.Attribute("maximum") != null) {
                option.Maximum = int.Parse(equipmentElement.Attribute("maximum").Value);
            }
            if (option.Default) {
                option.Equipped = true;
            }
            foreach (XElement replacementElement in equipmentElement.XPathSelectElements("item")) {
                EquipmentOption eo = this.ParseEquipmentOption(replacementElement, equipment);
                if (eo != null)
                    option.Replacements.Add(eo);
            }
            return option;
        }
    }
}
